package at.grabner.circleprogress;
import android.animation.TimeInterpolator;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.SweepGradient;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Message;
import android.support.annotation.ColorInt;
import android.support.annotation.FloatRange;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import java.text.DecimalFormat;
/**
* An circle view, similar to Android's ProgressBar.
* Can be used in 'value mode' or 'spinning mode'.
* <p/>
* In spinning mode it can be used like a intermediate progress bar.
* <p/>
* In value mode it can be used as a progress bar or to visualize any other value.
* Setting a value is fully animated. There are also nice transitions from animating to value mode.
* <p/>
* Typical use case would be to load a new value. During the loading time set the CircleView to spinning.
* As soon as you get your value, just set it with {@link #setValueAnimated(float, long)}.
*
* @author Jakob Grabner, based on the Progress wheel of Todd Davies
* https://github.com/Todd-Davies/CircleView
* <p/>
* Licensed under the Creative Commons Attribution 3.0 license see:
* http://creativecommons.org/licenses/by/3.0/
*/
@SuppressWarnings("unused")
public class CircleProgressView extends View {
/**
* The log tag.
*/
private final static String TAG = "CircleView";
private static final boolean DEBUG = false;
//----------------------------------
//region members
//Colors (with defaults)
private final int mBarColorStandard = 0xff009688; //stylish blue
protected int mLayoutHeight = 0;
protected int mLayoutWidth = 0;
//Rectangles
protected RectF mCircleBounds = new RectF();
protected RectF mInnerCircleBound = new RectF();
protected PointF mCenter;
/**
* Maximum size of the text.
*/
protected RectF mOuterTextBounds = new RectF();
/**
* Actual size of the text.
*/
protected RectF mActualTextBounds = new RectF();
protected RectF mUnitBounds = new RectF();
protected RectF mCircleOuterContour = new RectF();
protected RectF mCircleInnerContour = new RectF();
//value animation
Direction mDirection = Direction.CW;
float mCurrentValue = 0;
float mValueTo = 0;
float mValueFrom = 0;
float mMaxValue = 100;
float mMinValueAllowed = 0;
float mMaxValueAllowed = -1;
// spinner animation
float mSpinningBarLengthCurrent = 0;
float mSpinningBarLengthOrig = 42;
float mCurrentSpinnerDegreeValue = 0;
//Animation
//The amount of degree to move the bar by on each draw
float mSpinSpeed = 2.8f;
//Enable spin
boolean mSpin = false;
/**
* The animation duration in ms
*/
double mAnimationDuration = 900;
//The number of milliseconds to wait in between each draw
int mFrameDelayMillis = 10;
// helper for AnimationState.END_SPINNING_START_ANIMATING
boolean mDrawBarWhileSpinning;
//The animation handler containing the animation state machine.
AnimationHandler mAnimationHandler = new AnimationHandler(this);
//The current state of the animation state machine.
AnimationState mAnimationState = AnimationState.IDLE;
AnimationStateChangedListener mAnimationStateChangedListener;
private int mBarWidth = 40;
private int mRimWidth = 40;
private int mStartAngle = 270;
private float mOuterContourSize = 1;
private float mInnerContourSize = 1;
// Bar start/end width and type
private int mBarStartEndLineWidth = 0;
private BarStartEndLine mBarStartEndLine = BarStartEndLine.NONE;
private int mBarStartEndLineColor = 0xAA000000;
private float mBarStartEndLineSweep = 10f;
//Default text sizes
private int mUnitTextSize = 10;
private int mTextSize = 10;
//Text scale
private float mTextScale = 1;
private float mUnitScale = 1;
private int mOuterContourColor = 0xAA000000;
private int mInnerContourColor = 0xAA000000;
private int mSpinnerColor = mBarColorStandard; //stylish blue
private int mBackgroundCircleColor = 0x00000000; //transparent
private int mRimColor = 0xAA83d0c9;
private int mTextColor = 0xFF000000;
private int mUnitColor = 0xFF000000;
private boolean mIsAutoColorEnabled = false;
private int[] mBarColors = new int[]{
mBarColorStandard //stylish blue
};
//Caps
private Paint.Cap mBarStrokeCap = Paint.Cap.BUTT;
private Paint.Cap mSpinnerStrokeCap = Paint.Cap.BUTT;
//Paints
private Paint mBarPaint = new Paint();
private Paint mShaderlessBarPaint;
private Paint mBarSpinnerPaint = new Paint();
private Paint mBarStartEndLinePaint = new Paint();
private Paint mBackgroundCirclePaint = new Paint();
private Paint mRimPaint = new Paint();
private Paint mTextPaint = new Paint();
private Paint mUnitTextPaint = new Paint();
private Paint mOuterContourPaint = new Paint();
private Paint mInnerContourPaint = new Paint();
//Other
// The text to show
private String mText = "";
private int mTextLength;
private String mUnit = "";
private UnitPosition mUnitPosition = UnitPosition.RIGHT_TOP;
/**
* Indicates if the given text, the current percentage, or the current value should be shown.
*/
private TextMode mTextMode = TextMode.PERCENT;
private boolean mIsAutoTextSize;
private boolean mShowUnit = false;
//clipping
private Bitmap mClippingBitmap;
private Paint mMaskPaint;
/**
* Relative size of the unite string to the value string.
*/
private float mRelativeUniteSize = 1f;
private boolean mSeekModeEnabled = false;
private boolean mShowTextWhileSpinning = false;
private boolean mShowBlock = false;
private int mBlockCount = 18;
private float mBlockScale = 0.9f;
private float mBlockDegree = 360 / mBlockCount;
private float mBlockScaleDegree = mBlockDegree * mBlockScale;
private boolean mRoundToBlock = false;
private boolean mRoundToWholeNumber = false;
private int mTouchEventCount;
private OnProgressChangedListener onProgressChangedListener;
private float previousProgressChangedValue;
private DecimalFormat decimalFormat = new DecimalFormat("0");
// Text typeface
private Typeface textTypeface;
private Typeface unitTextTypeface;
//endregion members
//----------------------------------
/**
* The constructor for the CircleView
*
* @param context The context.
* @param attrs The attributes.
*/
public CircleProgressView(Context context, AttributeSet attrs) {
super(context, attrs);
parseAttributes(context.obtainStyledAttributes(attrs,
R.styleable.CircleProgressView));
if (!isInEditMode()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
}
mMaskPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mMaskPaint.setFilterBitmap(false);
mMaskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
setupPaints();
if (mSpin) {
spin();
}
}
private static float calcTextSizeForRect(String _text, Paint _textPaint, RectF _rectBounds) {
Matrix matrix = new Matrix();
Rect textBoundsTmp = new Rect();
//replace ones because for some fonts the 1 takes less space which causes issues
String text = _text.replace('1', '0');
//get current mText bounds
_textPaint.getTextBounds(text, 0, text.length(), textBoundsTmp);
RectF textBoundsTmpF = new RectF(textBoundsTmp);
matrix.setRectToRect(textBoundsTmpF, _rectBounds, Matrix.ScaleToFit.CENTER);
float values[] = new float[9];
matrix.getValues(values);
return _textPaint.getTextSize() * values[Matrix.MSCALE_X];
}
/**
* @param _angle The angle in degree to normalize
* @return the angle between 0 (EAST) and 360
*/
private static float normalizeAngle(float _angle) {
return (((_angle % 360) + 360) % 360);
}
/**
* Calculates the angle from centerPt to targetPt in degrees.
* The return should range from [0,360), rotating CLOCKWISE,
* 0 and 360 degrees represents EAST,
* 90 degrees represents SOUTH, etc...
* <p/>
* Assumes all points are in the same coordinate space. If they are not,
* you will need to call SwingUtilities.convertPointToScreen or equivalent
* on all arguments before passing them to this function.
*
* @param centerPt Point we are rotating around.
* @param targetPt Point we want to calculate the angle to.
* @return angle in degrees. This is the angle from centerPt to targetPt.
*/
public static double calcRotationAngleInDegrees(PointF centerPt, PointF targetPt) {
// calculate the angle theta from the deltaY and deltaX values
// (atan2 returns radians values from [-PI,PI])
// 0 currently points EAST.
// NOTE: By preserving Y and X param order to atan2, we are expecting
// a CLOCKWISE angle direction.
double theta = Math.atan2(targetPt.y - centerPt.y, targetPt.x - centerPt.x);
// rotate the theta angle clockwise by 90 degrees
// (this makes 0 point NORTH)
// NOTE: adding to an angle rotates it clockwise.
// subtracting would rotate it counter-clockwise
// theta += Math.PI/2.0;
// convert from radians to degrees
// this will give you an angle from [0->270],[-180,0]
double angle = Math.toDegrees(theta);
// convert to positive range [0-360)
// since we want to prevent negative angles, adjust them now.
// we can assume that atan2 will not return a negative value
// greater than one partial rotation
if (angle < 0) {
angle += 360;
}
return angle;
}
//----------------------------------
//region getter/setter
public BarStartEndLine getBarStartEndLine() {
return mBarStartEndLine;
}
/**
* Allows to add a line to the start/end of the bar
*
* @param _barWidth The width of the stroke on the start/end of the bar in pixel.
* @param _barStartEndLine The type of line on the start/end of the bar.
* @param _lineColor The line color
* @param _sweepWidth The sweep amount in degrees for the start and end bars to cover.
*/
public void setBarStartEndLine(int _barWidth, BarStartEndLine _barStartEndLine, @ColorInt int _lineColor, float _sweepWidth) {
mBarStartEndLineWidth = _barWidth;
mBarStartEndLine = _barStartEndLine;
mBarStartEndLineColor = _lineColor;
mBarStartEndLineSweep = _sweepWidth;
}
public int[] getBarColors() {
return mBarColors;
}
public Paint.Cap getBarStrokeCap() {
return mBarStrokeCap;
}
/**
* @param _barStrokeCap The stroke cap of the progress bar.
*/
public void setBarStrokeCap(Paint.Cap _barStrokeCap) {
mBarStrokeCap = _barStrokeCap;
mBarPaint.setStrokeCap(_barStrokeCap);
if (mBarStrokeCap != Paint.Cap.BUTT) {
mShaderlessBarPaint = new Paint(mBarPaint);
mShaderlessBarPaint.setShader(null);
mShaderlessBarPaint.setColor(mBarColors[0]);
}
}
public int getBarWidth() {
return mBarWidth;
}
/**
* @param barWidth The width of the progress bar in pixel.
*/
public void setBarWidth(@IntRange(from = 0) int barWidth) {
this.mBarWidth = barWidth;
mBarPaint.setStrokeWidth(barWidth);
mBarSpinnerPaint.setStrokeWidth(barWidth);
}
public int getBlockCount() {
return mBlockCount;
}
public void setBlockCount(int blockCount) {
if (blockCount > 1) {
mShowBlock = true;
mBlockCount = blockCount;
mBlockDegree = 360.0f / blockCount;
mBlockScaleDegree = mBlockDegree * mBlockScale;
} else {
mShowBlock = false;
}
}
public void setRoundToBlock(boolean _roundToBlock) {
mRoundToBlock = _roundToBlock;
}
public boolean getRoundToBlock() {
return mRoundToBlock;
}
public void setRoundToWholeNumber(boolean roundToWholeNumber) {
mRoundToWholeNumber = roundToWholeNumber;
}
public boolean getRoundToWholeNumber() {
return mRoundToWholeNumber;
}
public float getBlockScale() {
return mBlockScale;
}
public void setBlockScale(@FloatRange(from = 0.0, to = 1) float blockScale) {
if (blockScale >= 0.0f && blockScale <= 1.0f) {
mBlockScale = blockScale;
mBlockScaleDegree = mBlockDegree * blockScale;
}
}
public int getOuterContourColor() {
return mOuterContourColor;
}
/**
* @param _contourColor The color of the background contour of the circle.
*/
public void setOuterContourColor(@ColorInt int _contourColor) {
mOuterContourColor = _contourColor;
mOuterContourPaint.setColor(_contourColor);
}
public float getOuterContourSize() {
return mOuterContourSize;
}
/**
* @param _contourSize The size of the background contour of the circle.
*/
public void setOuterContourSize(@FloatRange(from = 0.0) float _contourSize) {
mOuterContourSize = _contourSize;
mOuterContourPaint.setStrokeWidth(_contourSize);
}
public int getInnerContourColor() {
return mInnerContourColor;
}
/**
* @param _contourColor The color of the background contour of the circle.
*/
public void setInnerContourColor(@ColorInt int _contourColor) {
mInnerContourColor = _contourColor;
mInnerContourPaint.setColor(_contourColor);
}
public float getInnerContourSize() {
return mInnerContourSize;
}
/**
* @param _contourSize The size of the background contour of the circle.
*/
public void setInnerContourSize(@FloatRange(from = 0.0) float _contourSize) {
mInnerContourSize = _contourSize;
mInnerContourPaint.setStrokeWidth(_contourSize);
}
/**
* @return The number of ms to wait between each draw call.
*/
public int getDelayMillis() {
return mFrameDelayMillis;
}
/**
* @param delayMillis The number of ms to wait between each draw call.
*/
public void setDelayMillis(int delayMillis) {
this.mFrameDelayMillis = delayMillis;
}
public int getFillColor() {
return mBackgroundCirclePaint.getColor();
}
public float getCurrentValue() {
return mCurrentValue;
}
public float getMinValueAllowed() {
return mMinValueAllowed;
}
public float getMaxValueAllowed() {
return mMaxValueAllowed;
}
public float getMaxValue() {
return mMaxValue;
}
/**
* The max value of the progress bar. Used to calculate the percentage of the current value.
* The bar fills according to the percentage. The default value is 100.
*
* @param _maxValue The max value.
*/
public void setMaxValue(@FloatRange(from = 0) float _maxValue) {
mMaxValue = _maxValue;
}
/**
* The min value allowed of the progress bar. Used to limit the min possible value of the current value.
*
* @param _minValueAllowed The min value allowed.
*/
public void setMinValueAllowed(@FloatRange(from = 0) float _minValueAllowed) {
mMinValueAllowed = _minValueAllowed;
}
/**
* The max value allowed of the progress bar. Used to limit the max possible value of the current value.
*
* @param _maxValueAllowed The max value allowed.
*/
public void setMaxValueAllowed(@FloatRange(from = 0) float _maxValueAllowed) {
mMaxValueAllowed = _maxValueAllowed;
}
/**
* @return The relative size (scale factor) of the unit text size to the text size
*/
public float getRelativeUniteSize() {
return mRelativeUniteSize;
}
public int getRimColor() {
return mRimColor;
}
/**
* @param rimColor The color of the rim around the Circle.
*/
public void setRimColor(@ColorInt int rimColor) {
mRimColor = rimColor;
mRimPaint.setColor(rimColor);
}
public Shader getRimShader() {
return mRimPaint.getShader();
}
public void setRimShader(Shader shader) {
this.mRimPaint.setShader(shader);
}
public int getRimWidth() {
return mRimWidth;
}
/**
* @param rimWidth The width in pixel of the rim around the circle
*/
public void setRimWidth(@IntRange(from = 0) int rimWidth) {
mRimWidth = rimWidth;
mRimPaint.setStrokeWidth(rimWidth);
}
public float getSpinSpeed() {
return mSpinSpeed;
}
/**
* The amount of degree to move the bar on every draw call.
*
* @param spinSpeed the speed of the spinner
*/
public void setSpinSpeed(float spinSpeed) {
mSpinSpeed = spinSpeed;
}
public Paint.Cap getSpinnerStrokeCap() {
return mSpinnerStrokeCap;
}
/**
* @param _spinnerStrokeCap The stroke cap of the progress bar in spinning mode.
*/
public void setSpinnerStrokeCap(Paint.Cap _spinnerStrokeCap) {
mSpinnerStrokeCap = _spinnerStrokeCap;
mBarSpinnerPaint.setStrokeCap(_spinnerStrokeCap);
}
public int getStartAngle() {
return mStartAngle;
}
public void setStartAngle(@IntRange(from = 0,to = 360) int _startAngle) {
// get a angle between 0 and 360
mStartAngle = (int) normalizeAngle(_startAngle);
}
public int calcTextColor() {
return mTextColor;
}
/**
* Sets the text color.
* You also need to set {@link #setTextColorAuto(boolean)} to false to see your color.
*
* @param textColor the color
*/
public void setTextColor(@ColorInt int textColor) {
mTextColor = textColor;
mTextPaint.setColor(textColor);
}
/**
* @return The scale value
*/
public float getTextScale() {
return mTextScale;
}
/**
* Scale factor for main text in the center of the circle view.
* Only used if auto text size is enabled.
*
* @param _textScale The scale value.
*/
public void setTextScale(@FloatRange(from = 0.0) float _textScale) {
mTextScale = _textScale;
}
public int getTextSize() {
return mTextSize;
}
/**
* Text size of the text string. Disables auto text size
* If auto text size is on, use {@link #setTextScale(float)} to scale textSize.
*
* @param textSize The text size of the unit.
*/
public void setTextSize(@IntRange(from = 0) int textSize) {
this.mTextPaint.setTextSize(textSize);
mTextSize = textSize;
mIsAutoTextSize = false;
}
public String getUnit() {
return mUnit;
}
/**
* @param _unit The unit to show next to the current value.
* You also need to set {@link #setUnitVisible(boolean)} to true.
*/
public void setUnit(String _unit) {
if (_unit == null) {
mUnit = "";
} else {
mUnit = _unit;
}
invalidate();
}
/**
* @return The scale value
*/
public float getUnitScale() {
return mUnitScale;
}
/**
* Scale factor for unit text next to the main text.
* Only used if auto text size is enabled.
*
* @param _unitScale The scale value.
*/
public void setUnitScale(@FloatRange(from = 0.0) float _unitScale) {
mUnitScale = _unitScale;
}
public int getUnitSize() {
return mUnitTextSize;
}
/**
* Text size of the unit string. Only used if text size is also set. (So automatic text size
* calculation is off. see {@link #setTextSize(int)}).
* If auto text size is on, use {@link #setUnitScale(float)} to scale unit size.
*
* @param unitSize The text size of the unit.
*/
public void setUnitSize(@IntRange(from = 0) int unitSize) {
mUnitTextSize = unitSize;
mUnitTextPaint.setTextSize(unitSize);
}
/**
* @return true if auto text size is enabled, false otherwise.
*/
public boolean isAutoTextSize() {
return mIsAutoTextSize;
}
/**
* @param _autoTextSize true to enable auto text size calculation.
*/
public void setAutoTextSize(boolean _autoTextSize) {
mIsAutoTextSize = _autoTextSize;
}
public boolean isSeekModeEnabled() {
return mSeekModeEnabled;
}
public void setSeekModeEnabled(boolean _seekModeEnabled) {
mSeekModeEnabled = _seekModeEnabled;
}
public boolean isShowBlock() {
return mShowBlock;
}
public void setShowBlock(boolean showBlock) {
mShowBlock = showBlock;
}
public boolean isShowTextWhileSpinning() {
return mShowTextWhileSpinning;
}
/**
* @param shouldDrawTextWhileSpinning True to show text in spinning mode, false to hide it.
*/
public void setShowTextWhileSpinning(boolean shouldDrawTextWhileSpinning) {
mShowTextWhileSpinning = shouldDrawTextWhileSpinning;
}
public boolean isUnitVisible() {
return mShowUnit;
}
/**
* @param _showUnit True to show unit, false to hide it.
*/
public void setUnitVisible(boolean _showUnit) {
if (_showUnit != mShowUnit) {
mShowUnit = _showUnit;
triggerReCalcTextSizesAndPositions(); // triggers recalculating text sizes
}
}
/**
* Sets the color of progress bar.
*
* @param barColors One or more colors. If more than one color is specified, a gradient of the colors is used.
*/
public void setBarColor(@ColorInt int... barColors) {
this.mBarColors = barColors;
setupBarPaint();
}
/**
* @param _clippingBitmap The bitmap used for clipping. Set to null to disable clipping.
* Default: No clipping.
*/
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void setClippingBitmap(Bitmap _clippingBitmap) {
if (getWidth() > 0 && getHeight() > 0) {
mClippingBitmap = Bitmap.createScaledBitmap(_clippingBitmap, getWidth(), getHeight(), false);
} else {
mClippingBitmap = _clippingBitmap;
}
if (mClippingBitmap == null) {
// enable HW acceleration
setLayerType(View.LAYER_TYPE_HARDWARE, null);
} else {
// disable HW acceleration
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
}
/**
* Sets the background color of the entire Progress Circle.
* Set the color to 0x00000000 (Color.TRANSPARENT) to hide it.
*
* @param circleColor the color.
*/
public void setFillCircleColor(@ColorInt int circleColor) {
mBackgroundCircleColor = circleColor;
mBackgroundCirclePaint.setColor(circleColor);
}
public void setOnAnimationStateChangedListener(AnimationStateChangedListener _animationStateChangedListener) {
mAnimationStateChangedListener = _animationStateChangedListener;
}
public void setOnProgressChangedListener(OnProgressChangedListener listener) {
onProgressChangedListener = listener;
}
/**
* @param _color The color of progress the bar in spinning mode.
*/
public void setSpinBarColor(@ColorInt int _color) {
mSpinnerColor = _color;
mBarSpinnerPaint.setColor(mSpinnerColor);
}
/**
* Length of spinning bar in degree.
*
* @param barLength length in degree
*/
public void setSpinningBarLength(@FloatRange(from = 0.0) float barLength) {
this.mSpinningBarLengthCurrent = mSpinningBarLengthOrig = barLength;
}
/**
* Set the text in the middle of the circle view.
* You need also set the {@link TextMode} to TextMode.TEXT to see the text.
*
* @param text The text to show
*/
public void setText(String text) {
mText = text != null ? text : "";
invalidate();
}
/**
* If auto text color is enabled, the text color and the unit color is always the same as the rim color.
* This is useful if the rim has multiple colors (color gradient), than the text will always have
* the color of the tip of the rim.
*
* @param isEnabled true to enable, false to disable
*/
public void setTextColorAuto(boolean isEnabled) {
mIsAutoColorEnabled = isEnabled;
}
/**
* Sets the auto text mode.
*
* @param _textValue The mode
*/
public void setTextMode(TextMode _textValue) {
mTextMode = _textValue;
}
/**
* @param typeface The typeface to use for the text
*/
public void setTextTypeface(Typeface typeface) {
mTextPaint.setTypeface(typeface);
}
/**
* Sets the unit text color.
* Also sets {@link #setTextColorAuto(boolean)} to false
*
* @param unitColor The color.
*/
public void setUnitColor(@ColorInt int unitColor) {
mUnitColor = unitColor;
mUnitTextPaint.setColor(unitColor);
mIsAutoColorEnabled = false;
}
public void setUnitPosition(UnitPosition _unitPosition) {
mUnitPosition = _unitPosition;
triggerReCalcTextSizesAndPositions(); // triggers recalculating text sizes
}
/**
* @param typeface The typeface to use for the unit text
*/
public void setUnitTextTypeface(Typeface typeface) {
mUnitTextPaint.setTypeface(typeface);
}
/**
* @param _relativeUniteSize The relative scale factor of the unit text size to the text size.
* Only useful for autotextsize=true; Effects both, the unit text size and the text size.
*/
public void setUnitToTextScale(@FloatRange(from = 0.0) float _relativeUniteSize) {
mRelativeUniteSize = _relativeUniteSize;
triggerReCalcTextSizesAndPositions();
}
/**
* Sets the direction of circular motion (clockwise or counter-clockwise).
*/
public void setDirection(Direction direction) {
mDirection = direction;
}
/**
* Set the value of the circle view without an animation.
* Stops any currently active animations.
*
* @param _value The value.
*/
public void setValue(float _value) {
// round to block
if (mShowBlock && mRoundToBlock) {
float value_per_block = mMaxValue / (float) mBlockCount;
_value = Math.round(_value / value_per_block) * value_per_block;
} else if (mRoundToWholeNumber) { // round to whole number
_value = Math.round(_value);
}
// respect min and max values allowed
_value = Math.max(mMinValueAllowed, _value);
if (mMaxValueAllowed >= 0)
_value = Math.min(mMaxValueAllowed, _value);
Message msg = new Message();
msg.what = AnimationMsg.SET_VALUE.ordinal();
msg.obj = new float[]{_value, _value};
mAnimationHandler.sendMessage(msg);
triggerOnProgressChanged(_value);
}
/**
* Sets the value of the circle view with an animation.
* The current value is used as the start value of the animation
*
* @param _valueTo value after animation
*/
public void setValueAnimated(float _valueTo) {
setValueAnimated(_valueTo, 1200);
}
/**
* Sets the value of the circle view with an animation.
* The current value is used as the start value of the animation
*
* @param _valueTo value after animation
* @param _animationDuration the duration of the animation in milliseconds.
*/
public void setValueAnimated(float _valueTo, long _animationDuration) {
setValueAnimated(mCurrentValue, _valueTo, _animationDuration);
}
/**
* Sets the value of the circle view with an animation.
*
* @param _valueFrom start value of the animation
* @param _valueTo value after animation
* @param _animationDuration the duration of the animation in milliseconds
*/
public void setValueAnimated(float _valueFrom, float _valueTo, long _animationDuration) {
// round to block
if (mShowBlock && mRoundToBlock) {
float value_per_block = mMaxValue / (float) mBlockCount;
_valueTo = Math.round(_valueTo / value_per_block) * value_per_block;
} else if (mRoundToWholeNumber) {
_valueTo = Math.round(_valueTo);
}
// respect min and max values allowed
_valueTo = Math.max(mMinValueAllowed, _valueTo);
if (mMaxValueAllowed >= 0)
_valueTo = Math.min(mMaxValueAllowed, _valueTo);
mAnimationDuration = _animationDuration;
Message msg = new Message();
msg.what = AnimationMsg.SET_VALUE_ANIMATED.ordinal();
msg.obj = new float[]{_valueFrom, _valueTo};
mAnimationHandler.sendMessage(msg);
triggerOnProgressChanged(_valueTo);
}
public DecimalFormat getDecimalFormat() {
return decimalFormat;
}
public void setDecimalFormat(DecimalFormat decimalFormat) {
if (decimalFormat == null) {
throw new IllegalArgumentException("decimalFormat must not be null!");
}
this.decimalFormat = decimalFormat;
}
/**
* Sets interpolator for value animations.
*
* @param interpolator the interpolator
*/
public void setValueInterpolator(TimeInterpolator interpolator) {
mAnimationHandler.setValueInterpolator(interpolator);
}
/**
* Sets the interpolator for length changes of the bar.
*
* @param interpolator the interpolator
*/
public void setLengthChangeInterpolator(TimeInterpolator interpolator) {
mAnimationHandler.setLengthChangeInterpolator(interpolator);
}
//endregion getter/setter
//----------------------------------
/**
* Parse the attributes passed to the view from the XML
*
* @param a the attributes to parse
*/
private void parseAttributes(TypedArray a) {
setBarWidth((int) a.getDimension(R.styleable.CircleProgressView_cpv_barWidth,
mBarWidth));
setRimWidth((int) a.getDimension(R.styleable.CircleProgressView_cpv_rimWidth,
mRimWidth));
setSpinSpeed((int) a.getFloat(R.styleable.CircleProgressView_cpv_spinSpeed,
mSpinSpeed));
setSpin(a.getBoolean(R.styleable.CircleProgressView_cpv_spin,
mSpin));
setDirection(Direction.values()[a.getInt(R.styleable.CircleProgressView_cpv_direction, 0)]);
float value = a.getFloat(R.styleable.CircleProgressView_cpv_value, mCurrentValue);
setValue(value);
mCurrentValue = value;
if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor2) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor3)) {
mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor2, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor3, mBarColorStandard)};
} else if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor2)) {
mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor2, mBarColorStandard)};
} else if (a.hasValue(R.styleable.CircleProgressView_cpv_barColor) && a.hasValue(R.styleable.CircleProgressView_cpv_barColor1)) {
mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor1, mBarColorStandard)};
} else {
mBarColors = new int[]{a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard), a.getColor(R.styleable.CircleProgressView_cpv_barColor, mBarColorStandard)};
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_barStrokeCap)) {
setBarStrokeCap(StrokeCap.values()[a.getInt(R.styleable.CircleProgressView_cpv_barStrokeCap, 0)].paintCap);
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_barStartEndLineWidth) && a.hasValue(R.styleable.CircleProgressView_cpv_barStartEndLine)) {
setBarStartEndLine((int) a.getDimension(R.styleable.CircleProgressView_cpv_barStartEndLineWidth, 0),
BarStartEndLine.values()[a.getInt(R.styleable.CircleProgressView_cpv_barStartEndLine, 3)],
a.getColor(R.styleable.CircleProgressView_cpv_barStartEndLineColor, mBarStartEndLineColor),
a.getFloat(R.styleable.CircleProgressView_cpv_barStartEndLineSweep, mBarStartEndLineSweep));
}
setSpinBarColor(a.getColor(R.styleable.CircleProgressView_cpv_spinColor, mSpinnerColor));
setSpinningBarLength(a.getFloat(R.styleable.CircleProgressView_cpv_spinBarLength,
mSpinningBarLengthOrig));
if (a.hasValue(R.styleable.CircleProgressView_cpv_textSize)) {
setTextSize((int) a.getDimension(R.styleable.CircleProgressView_cpv_textSize, mTextSize));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_unitSize)) {
setUnitSize((int) a.getDimension(R.styleable.CircleProgressView_cpv_unitSize, mUnitTextSize));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_textColor)) {
setTextColor(a.getColor(R.styleable.CircleProgressView_cpv_textColor, mTextColor));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_unitColor)) {
setUnitColor(a.getColor(R.styleable.CircleProgressView_cpv_unitColor, mUnitColor));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_autoTextColor)) {
setTextColorAuto(a.getBoolean(R.styleable.CircleProgressView_cpv_autoTextColor, mIsAutoColorEnabled));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_autoTextSize)) {
setAutoTextSize(a.getBoolean(R.styleable.CircleProgressView_cpv_autoTextSize, mIsAutoTextSize));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_textMode)) {
setTextMode(TextMode.values()[a.getInt(R.styleable.CircleProgressView_cpv_textMode, 0)]);
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_unitPosition)) {
setUnitPosition(UnitPosition.values()[a.getInt(R.styleable.CircleProgressView_cpv_unitPosition, 3)]);
}
//if the mText is empty, show current percentage value
if (a.hasValue(R.styleable.CircleProgressView_cpv_text)) {
setText(a.getString(R.styleable.CircleProgressView_cpv_text));
}
setUnitToTextScale(a.getFloat(R.styleable.CircleProgressView_cpv_unitToTextScale, 1f));
setRimColor(a.getColor(R.styleable.CircleProgressView_cpv_rimColor,
mRimColor));
setFillCircleColor(a.getColor(R.styleable.CircleProgressView_cpv_fillColor,
mBackgroundCircleColor));
setOuterContourColor(a.getColor(R.styleable.CircleProgressView_cpv_outerContourColor, mOuterContourColor));
setOuterContourSize(a.getDimension(R.styleable.CircleProgressView_cpv_outerContourSize, mOuterContourSize));
setInnerContourColor(a.getColor(R.styleable.CircleProgressView_cpv_innerContourColor, mInnerContourColor));
setInnerContourSize(a.getDimension(R.styleable.CircleProgressView_cpv_innerContourSize, mInnerContourSize));
setMaxValue(a.getFloat(R.styleable.CircleProgressView_cpv_maxValue, mMaxValue));
setMinValueAllowed(a.getFloat(R.styleable.CircleProgressView_cpv_minValueAllowed, mMinValueAllowed));
setMaxValueAllowed(a.getFloat(R.styleable.CircleProgressView_cpv_maxValueAllowed, mMaxValueAllowed));
setRoundToBlock(a.getBoolean(R.styleable.CircleProgressView_cpv_roundToBlock, mRoundToBlock));
setRoundToWholeNumber(a.getBoolean(R.styleable.CircleProgressView_cpv_roundToWholeNumber, mRoundToWholeNumber));
setUnit(a.getString(R.styleable.CircleProgressView_cpv_unit));
setUnitVisible(a.getBoolean(R.styleable.CircleProgressView_cpv_showUnit, mShowUnit));
setTextScale(a.getFloat(R.styleable.CircleProgressView_cpv_textScale, mTextScale));
setUnitScale(a.getFloat(R.styleable.CircleProgressView_cpv_unitScale, mUnitScale));
setSeekModeEnabled(a.getBoolean(R.styleable.CircleProgressView_cpv_seekMode, mSeekModeEnabled));
setStartAngle(a.getInt(R.styleable.CircleProgressView_cpv_startAngle, mStartAngle));
setShowTextWhileSpinning(a.getBoolean(R.styleable.CircleProgressView_cpv_showTextInSpinningMode, mShowTextWhileSpinning));
if (a.hasValue(R.styleable.CircleProgressView_cpv_blockCount)) {
setBlockCount(a.getInt(R.styleable.CircleProgressView_cpv_blockCount, 1));
setBlockScale(a.getFloat(R.styleable.CircleProgressView_cpv_blockScale, 0.9f));
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_textTypeface)) {
try {
textTypeface = Typeface.createFromAsset(getContext().getAssets(), a.getString(R.styleable.CircleProgressView_cpv_textTypeface));
} catch (Exception exception) {
// error while trying to inflate typeface (is the path set correctly?)
}
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_unitTypeface)) {
try {
unitTextTypeface = Typeface.createFromAsset(getContext().getAssets(), a.getString(R.styleable.CircleProgressView_cpv_unitTypeface));
} catch (Exception exception) {
// error while trying to inflate typeface (is the path set correctly?)
}
}
if (a.hasValue(R.styleable.CircleProgressView_cpv_decimalFormat)) {
try {
String pattern = a.getString(R.styleable.CircleProgressView_cpv_decimalFormat);
if (pattern != null) {
decimalFormat = new DecimalFormat(pattern);
}
} catch (Exception exception) {
Log.w(TAG, exception.getMessage());
}
}
// Recycle
a.recycle();
}
/*
* When this is called, make the view square.
* From: http://www.jayway.com/2012/12/12/creating-custom-android-views-part-4-measuring-and-how-to-force-a-view-to-be-square/
*
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// The first thing that happen is that we call the superclass
// implementation of onMeasure. The reason for that is that measuring
// can be quite a complex process and calling the super method is a
// convenient way to get most of this complexity handled.
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// We can’t use getWidth() or getHeight() here. During the measuring
// pass the view has not gotten its final size yet (this happens first
// at the start of the layout pass) so we have to use getMeasuredWidth()
// and getMeasuredHeight().
int size;
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
int heightWithoutPadding = height - getPaddingTop() - getPaddingBottom();
// Finally we have some simple logic that calculates the size of the view
// and calls setMeasuredDimension() to set that size.
// Before we compare the width and height of the view, we remove the padding,
// and when we set the dimension we add it back again. Now the actual content
// of the view will be square, but, depending on the padding, the total dimensions
// of the view might not be.
if (widthWithoutPadding > heightWithoutPadding) {
size = heightWithoutPadding;
} else {
size = widthWithoutPadding;
}
// If you override onMeasure() you have to call setMeasuredDimension().
// This is how you report back the measured size. If you don’t call
// setMeasuredDimension() the parent will throw an exception and your
// application will crash.
// We are calling the onMeasure() method of the superclass so we don’t
// actually need to call setMeasuredDimension() since that takes care
// of that. However, the purpose with overriding onMeasure() was to
// change the default behaviour and to do that we need to call
// setMeasuredDimension() with our own values.
setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), size + getPaddingTop() + getPaddingBottom());
}
/**
* Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view,
* because this method is called after measuring the dimensions of MATCH_PARENT and WRAP_CONTENT.
* Use this dimensions to setup the bounds and paints.
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// Share the dimensions
mLayoutWidth = w;
mLayoutHeight = h;
setupBounds();
setupBarPaint();
if (mClippingBitmap != null) {
mClippingBitmap = Bitmap.createScaledBitmap(mClippingBitmap, getWidth(), getHeight(), false);
}
invalidate();
}
//----------------------------------
// region helper
private float calcTextSizeForCircle(String _text, Paint _textPaint, RectF _circleBounds) {
//get mActualTextBounds bounds
RectF innerCircleBounds = getInnerCircleRect(_circleBounds);
return calcTextSizeForRect(_text, _textPaint, innerCircleBounds);
}
private RectF getInnerCircleRect(RectF _circleBounds) {
double circleWidth = +_circleBounds.width() - (Math.max(mBarWidth, mRimWidth)) - mOuterContourSize - mInnerContourSize;
double width = ((circleWidth / 2d) * Math.sqrt(2d));
float widthDelta = (_circleBounds.width() - (float) width) / 2f;
float scaleX = 1;
float scaleY = 1;
if (isUnitVisible()) {
switch (mUnitPosition) {
case TOP:
case BOTTOM:
scaleX = 1.1f; // scaleX square to rectangle, so the longer text with unit fits better
scaleY = 0.88f;
break;
case LEFT_TOP:
case RIGHT_TOP:
case LEFT_BOTTOM:
case RIGHT_BOTTOM:
scaleX = 0.77f; // scaleX square to rectangle, so the longer text with unit fits better
scaleY = 1.33f;
break;
}
}
return new RectF(_circleBounds.left + (widthDelta * scaleX), _circleBounds.top + (widthDelta * scaleY), _circleBounds.right - (widthDelta * scaleX), _circleBounds.bottom - (widthDelta * scaleY));
}
private void triggerOnProgressChanged(float value) {
if (onProgressChangedListener != null && value != previousProgressChangedValue) {
onProgressChangedListener.onProgressChanged(value);
previousProgressChangedValue = value;
}
}
private void triggerReCalcTextSizesAndPositions() {
mTextLength = -1;
mOuterTextBounds = getInnerCircleRect(mCircleBounds);
invalidate();
}
private int calcTextColor(double value) {
if (mBarColors.length > 1) {
double percent = 1f / getMaxValue() * value;
int low = (int) Math.floor((mBarColors.length - 1) * percent);
int high = low + 1;
if (low < 0) {
low = 0;
high = 1;
} else if (high >= mBarColors.length) {
low = mBarColors.length - 2;
high = mBarColors.length - 1;
}
return ColorUtils.getRGBGradient(mBarColors[low], mBarColors[high], (float) (1 - (((mBarColors.length - 1) * percent) % 1d)));
} else if (mBarColors.length == 1) {
return mBarColors[0];
} else {
return Color.BLACK;
}
}
private void setTextSizeAndTextBoundsWithAutoTextSize(float unitGapWidthHalf, float unitWidth, float unitGapHeightHalf, float unitHeight, String text) {
RectF textRect = mOuterTextBounds;
if (mShowUnit) {
//shrink text Rect so that there is space for the unit
switch (mUnitPosition) {
case TOP:
textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top + unitHeight + unitGapHeightHalf, mOuterTextBounds.right, mOuterTextBounds.bottom);
break;
case BOTTOM:
textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.bottom - unitHeight - unitGapHeightHalf);
break;
case LEFT_TOP:
case LEFT_BOTTOM:
textRect = new RectF(mOuterTextBounds.left + unitWidth + unitGapWidthHalf, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.bottom);
break;
case RIGHT_TOP:
case RIGHT_BOTTOM:
default:
textRect = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right - unitWidth - unitGapWidthHalf, mOuterTextBounds.bottom);
break;
}
}
mTextPaint.setTextSize(calcTextSizeForRect(text, mTextPaint, textRect) * mTextScale);
mActualTextBounds = calcTextBounds(text, mTextPaint, textRect); // center text in text rect
}
private void setTextSizeAndTextBoundsWithFixedTextSize(String text) {
mTextPaint.setTextSize(mTextSize);
mActualTextBounds = calcTextBounds(text, mTextPaint, mCircleBounds); //center text in circle
}
private void setUnitTextBoundsAndSizeWithAutoTextSize(float unitGapWidthHalf, float unitWidth, float unitGapHeightHalf, float unitHeight) {
//calc the rectangle containing the unit text
switch (mUnitPosition) {
case TOP: {
mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.top + unitHeight - unitGapHeightHalf);
break;
}
case BOTTOM:
mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.bottom - unitHeight + unitGapHeightHalf, mOuterTextBounds.right, mOuterTextBounds.bottom);
break;
case LEFT_TOP:
case LEFT_BOTTOM: {
mUnitBounds = new RectF(mOuterTextBounds.left, mOuterTextBounds.top, mOuterTextBounds.left + unitWidth - unitGapWidthHalf, mOuterTextBounds.top + unitHeight);
break;
}
case RIGHT_TOP:
case RIGHT_BOTTOM:
default: {
mUnitBounds = new RectF(mOuterTextBounds.right - unitWidth + unitGapWidthHalf, mOuterTextBounds.top, mOuterTextBounds.right, mOuterTextBounds.top + unitHeight);
}
break;
}
mUnitTextPaint.setTextSize(calcTextSizeForRect(mUnit, mUnitTextPaint, mUnitBounds) * mUnitScale);
mUnitBounds = calcTextBounds(mUnit, mUnitTextPaint, mUnitBounds); // center text in rectangle and reuse it
switch (mUnitPosition) {
case LEFT_TOP:
case RIGHT_TOP: {
//move unite to top of text
float dy = mActualTextBounds.top - mUnitBounds.top;
mUnitBounds.offset(0, dy);
break;
}
case LEFT_BOTTOM:
case RIGHT_BOTTOM: {
//move unite to bottom of text
float dy = mActualTextBounds.bottom - mUnitBounds.bottom;
mUnitBounds.offset(0, dy);
break;
}
}
}
private void setUnitTextBoundsAndSizeWithFixedTextSize(float unitGapWidth, float unitGapHeight) {
mUnitTextPaint.setTextSize(mUnitTextSize);
mUnitBounds = calcTextBounds(mUnit, mUnitTextPaint, mOuterTextBounds); // center text in rectangle and reuse it
switch (mUnitPosition) {
case TOP:
mUnitBounds.offsetTo(mUnitBounds.left, mActualTextBounds.top - unitGapHeight - mUnitBounds.height());
break;
case BOTTOM:
mUnitBounds.offsetTo(mUnitBounds.left, mActualTextBounds.bottom + unitGapHeight);
break;
case LEFT_TOP:
case LEFT_BOTTOM:
mUnitBounds.offsetTo(mActualTextBounds.left - unitGapWidth - mUnitBounds.width(), mUnitBounds.top);
break;
case RIGHT_TOP:
case RIGHT_BOTTOM:
default:
mUnitBounds.offsetTo(mActualTextBounds.right + unitGapWidth, mUnitBounds.top);
break;
}
switch (mUnitPosition) {
case LEFT_TOP:
case RIGHT_TOP: {
//move unite to top of text
float dy = mActualTextBounds.top - mUnitBounds.top;
mUnitBounds.offset(0, dy);
break;
}
case LEFT_BOTTOM:
case RIGHT_BOTTOM: {
//move unite to bottom of text
float dy = mActualTextBounds.bottom - mUnitBounds.bottom;
mUnitBounds.offset(0, dy);
break;
}
}
}
/**
* Returns the bounding rectangle of the given _text, with the size and style defined in the _textPaint centered in the middle of the _textBounds
*
* @param _text The text.
* @param _textPaint The paint defining the text size and style.
* @param _textBounds The rect where the text will be centered.
* @return The bounding box of the text centered in the _textBounds.
*/
private RectF calcTextBounds(String _text, Paint _textPaint, RectF _textBounds) {
Rect textBoundsTmp = new Rect();
//get current text bounds
_textPaint.getTextBounds(_text, 0, _text.length(), textBoundsTmp);
float width = textBoundsTmp.left + textBoundsTmp.width();
float height = textBoundsTmp.bottom + textBoundsTmp.height() * 0.93f; // the height of calcTextBounds is a bit to high, therefore * 0.93
//center in circle
RectF textRect = new RectF();
textRect.left = (_textBounds.left + ((_textBounds.width() - width) / 2));
textRect.top = _textBounds.top + ((_textBounds.height() - height) / 2);
textRect.right = textRect.left + width;
textRect.bottom = textRect.top + height;
return textRect;
}
//endregion helper
//----------------------------------
//----------------------------------
//region Setting up stuff
/**
* Set the bounds of the component
*/
private void setupBounds() {
// Width should equal to Height, find the min value to setup the circle
int minValue = Math.min(mLayoutWidth, mLayoutHeight);
// Calc the Offset if needed
int xOffset = mLayoutWidth - minValue;
int yOffset = mLayoutHeight - minValue;
// Add the offset
float paddingTop = this.getPaddingTop() + (yOffset / 2);
float paddingBottom = this.getPaddingBottom() + (yOffset / 2);
float paddingLeft = this.getPaddingLeft() + (xOffset / 2);
float paddingRight = this.getPaddingRight() + (xOffset / 2);
int width = getWidth(); //this.getLayoutParams().width;
int height = getHeight(); //this.getLayoutParams().height;
float circleWidthHalf = mBarWidth / 2f > mRimWidth / 2f + mOuterContourSize ? mBarWidth / 2f : mRimWidth / 2f + mOuterContourSize;
mCircleBounds = new RectF(paddingLeft + circleWidthHalf,
paddingTop + circleWidthHalf,
width - paddingRight - circleWidthHalf,
height - paddingBottom - circleWidthHalf);
mInnerCircleBound = new RectF(paddingLeft + (mBarWidth),
paddingTop + (mBarWidth),
width - paddingRight - (mBarWidth),
height - paddingBottom - (mBarWidth));
mOuterTextBounds = getInnerCircleRect(mCircleBounds);
mCircleInnerContour = new RectF(mCircleBounds.left + (mRimWidth / 2.0f) + (mInnerContourSize / 2.0f), mCircleBounds.top + (mRimWidth / 2.0f) + (mInnerContourSize / 2.0f), mCircleBounds.right - (mRimWidth / 2.0f) - (mInnerContourSize / 2.0f), mCircleBounds.bottom - (mRimWidth / 2.0f) - (mInnerContourSize / 2.0f));
mCircleOuterContour = new RectF(mCircleBounds.left - (mRimWidth / 2.0f) - (mOuterContourSize / 2.0f), mCircleBounds.top - (mRimWidth / 2.0f) - (mOuterContourSize / 2.0f), mCircleBounds.right + (mRimWidth / 2.0f) + (mOuterContourSize / 2.0f), mCircleBounds.bottom + (mRimWidth / 2.0f) + (mOuterContourSize / 2.0f));
mCenter = new PointF(mCircleBounds.centerX(), mCircleBounds.centerY());
}
private void setupBarPaint() {
if (mBarColors.length > 1) {
mBarPaint.setShader(new SweepGradient(mCircleBounds.centerX(), mCircleBounds.centerY(), mBarColors, null));
Matrix matrix = new Matrix();
mBarPaint.getShader().getLocalMatrix(matrix);
matrix.postTranslate(-mCircleBounds.centerX(), -mCircleBounds.centerY());
matrix.postRotate(mStartAngle);
matrix.postTranslate(mCircleBounds.centerX(), mCircleBounds.centerY());
mBarPaint.getShader().setLocalMatrix(matrix);
mBarPaint.setColor(mBarColors[0]);
} else if (mBarColors.length == 1) {
mBarPaint.setColor(mBarColors[0]);
mBarPaint.setShader(null);
} else {
mBarPaint.setColor(mBarColorStandard);
mBarPaint.setShader(null);
}
mBarPaint.setAntiAlias(true);
mBarPaint.setStrokeCap(mBarStrokeCap);
mBarPaint.setStyle(Style.STROKE);
mBarPaint.setStrokeWidth(mBarWidth);
if (mBarStrokeCap != Paint.Cap.BUTT) {
mShaderlessBarPaint = new Paint(mBarPaint);
mShaderlessBarPaint.setShader(null);
mShaderlessBarPaint.setColor(mBarColors[0]);
}
}
/**
* Setup all paints.
* Call only if changes to color or size properties are not visible.
*/
public void setupPaints() {
setupBarPaint();
setupBarSpinnerPaint();
setupOuterContourPaint();
setupInnerContourPaint();
setupUnitTextPaint();
setupTextPaint();
setupBackgroundCirclePaint();
setupRimPaint();
setupBarStartEndLinePaint();
}
private void setupBarStartEndLinePaint() {
mBarStartEndLinePaint.setColor(mBarStartEndLineColor);
mBarStartEndLinePaint.setAntiAlias(true);
mBarStartEndLinePaint.setStyle(Style.STROKE);
mBarStartEndLinePaint.setStrokeWidth(mBarStartEndLineWidth);
}
private void setupOuterContourPaint() {
mOuterContourPaint.setColor(mOuterContourColor);
mOuterContourPaint.setAntiAlias(true);
mOuterContourPaint.setStyle(Style.STROKE);
mOuterContourPaint.setStrokeWidth(mOuterContourSize);
}
private void setupInnerContourPaint() {
mInnerContourPaint.setColor(mInnerContourColor);
mInnerContourPaint.setAntiAlias(true);
mInnerContourPaint.setStyle(Style.STROKE);
mInnerContourPaint.setStrokeWidth(mInnerContourSize);
}
private void setupUnitTextPaint() {
mUnitTextPaint.setStyle(Style.FILL);
mUnitTextPaint.setAntiAlias(true);
if (unitTextTypeface != null) {
mUnitTextPaint.setTypeface(unitTextTypeface);
}
}
private void setupTextPaint() {
mTextPaint.setSubpixelText(true);
mTextPaint.setLinearText(true);
mTextPaint.setTypeface(Typeface.MONOSPACE);
mTextPaint.setColor(mTextColor);
mTextPaint.setStyle(Style.FILL);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextSize(mTextSize);
if (textTypeface != null) {
mTextPaint.setTypeface(textTypeface);
} else {
mTextPaint.setTypeface(Typeface.MONOSPACE);
}
}
private void setupBackgroundCirclePaint() {
mBackgroundCirclePaint.setColor(mBackgroundCircleColor);
mBackgroundCirclePaint.setAntiAlias(true);
mBackgroundCirclePaint.setStyle(Style.FILL);
}
private void setupRimPaint() {
mRimPaint.setColor(mRimColor);
mRimPaint.setAntiAlias(true);
mRimPaint.setStyle(Style.STROKE);
mRimPaint.setStrokeWidth(mRimWidth);
}
private void setupBarSpinnerPaint() {
mBarSpinnerPaint.setAntiAlias(true);
mBarSpinnerPaint.setStrokeCap(mSpinnerStrokeCap);
mBarSpinnerPaint.setStyle(Style.STROKE);
mBarSpinnerPaint.setStrokeWidth(mBarWidth);
mBarSpinnerPaint.setColor(mSpinnerColor);
}
//endregion Setting up stuff
//----------------------------------
//----------------------------------
//region draw all the things
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (DEBUG) {
drawDebug(canvas);
}
float degrees = (360f / mMaxValue * mCurrentValue);
// Draw the background circle
if (mBackgroundCircleColor != 0) {
canvas.drawArc(mInnerCircleBound, 360, 360, false, mBackgroundCirclePaint);
}
//Draw the rim
if (mRimWidth > 0) {
if (!mShowBlock) {
canvas.drawArc(mCircleBounds, 360, 360, false, mRimPaint);
} else {
drawBlocks(canvas, mCircleBounds, mStartAngle, 360, false, mRimPaint);
}
}
//Draw outer contour
if (mOuterContourSize > 0) {
canvas.drawArc(mCircleOuterContour, 360, 360, false, mOuterContourPaint);
}
//Draw outer contour
if (mInnerContourSize > 0) {
canvas.drawArc(mCircleInnerContour, 360, 360, false, mInnerContourPaint);
}
//Draw spinner
if (mAnimationState == AnimationState.SPINNING || mAnimationState == AnimationState.END_SPINNING) {
drawSpinner(canvas);
if (mShowTextWhileSpinning) {
drawTextWithUnit(canvas);
}
} else if (mAnimationState == AnimationState.END_SPINNING_START_ANIMATING) {
//draw spinning arc
drawSpinner(canvas);
if (mDrawBarWhileSpinning) {
drawBar(canvas, degrees);
drawTextWithUnit(canvas);
} else if (mShowTextWhileSpinning) {
drawTextWithUnit(canvas);
}
} else {
drawBar(canvas, degrees);
drawTextWithUnit(canvas);
}
if (mClippingBitmap != null) {
canvas.drawBitmap(mClippingBitmap, 0, 0, mMaskPaint);
}
if (mBarStartEndLineWidth > 0 && mBarStartEndLine != BarStartEndLine.NONE) {
drawStartEndLine(canvas, degrees);
}
}
private void drawStartEndLine(Canvas _canvas, float _degrees) {
if (_degrees == 0f)
return;
float startAngle = mDirection == Direction.CW ? mStartAngle : mStartAngle - _degrees;
startAngle -= mBarStartEndLineSweep / 2f;
if (mBarStartEndLine == BarStartEndLine.START || mBarStartEndLine == BarStartEndLine.BOTH) {
_canvas.drawArc(mCircleBounds, startAngle, mBarStartEndLineSweep, false, mBarStartEndLinePaint);
}
if (mBarStartEndLine == BarStartEndLine.END || mBarStartEndLine == BarStartEndLine.BOTH) {
_canvas.drawArc(mCircleBounds, startAngle + _degrees, mBarStartEndLineSweep, false, mBarStartEndLinePaint);
}
}
private void drawDebug(Canvas canvas) {
Paint innerRectPaint = new Paint();
innerRectPaint.setColor(Color.YELLOW);
canvas.drawRect(mCircleBounds, innerRectPaint);
}
private void drawBlocks(Canvas _canvas, RectF circleBounds, float startAngle, float _degrees, boolean userCenter, Paint paint) {
float tmpDegree = 0.0f;
while (tmpDegree < _degrees) {
_canvas.drawArc(circleBounds, startAngle + tmpDegree, Math.min(mBlockScaleDegree, _degrees - tmpDegree), userCenter, paint);
tmpDegree += mBlockDegree;
}
}
private void drawSpinner(Canvas canvas) {
if (mSpinningBarLengthCurrent < 0) {
mSpinningBarLengthCurrent = 1;
}
float startAngle;
if (mDirection == Direction.CW) {
startAngle = mStartAngle + mCurrentSpinnerDegreeValue - mSpinningBarLengthCurrent;
} else {
startAngle = mStartAngle - mCurrentSpinnerDegreeValue;
}
canvas.drawArc(mCircleBounds, startAngle, mSpinningBarLengthCurrent, false,
mBarSpinnerPaint);
}
private void drawTextWithUnit(Canvas canvas) {
final float relativeGapHeight;
final float relativeGapWidth;
final float relativeHeight;
final float relativeWidth;
switch (mUnitPosition) {
case TOP:
case BOTTOM:
relativeGapWidth = 0.05f; //gap size between text and unit
relativeGapHeight = 0.025f; //gap size between text and unit
relativeHeight = 0.25f * mRelativeUniteSize;
relativeWidth = 0.4f * mRelativeUniteSize;
break;
default:
case LEFT_TOP:
case RIGHT_TOP:
case LEFT_BOTTOM:
case RIGHT_BOTTOM:
relativeGapWidth = 0.05f; //gap size between text and unit
relativeGapHeight = 0.025f; //gap size between text and unit
relativeHeight = 0.55f * mRelativeUniteSize;
relativeWidth = 0.3f * mRelativeUniteSize;
break;
}
float unitGapWidthHalf = mOuterTextBounds.width() * relativeGapWidth / 2f;
float unitWidth = (mOuterTextBounds.width() * relativeWidth);
float unitGapHeightHalf = mOuterTextBounds.height() * relativeGapHeight / 2f;
float unitHeight = (mOuterTextBounds.height() * relativeHeight);
boolean update = false;
//Draw Text
if (mIsAutoColorEnabled) {
mTextPaint.setColor(calcTextColor(mCurrentValue));
}
//set text
String text;
switch (mTextMode) {
case TEXT:
default:
text = mText != null ? mText : "";
break;
case PERCENT:
text = decimalFormat.format(100f / mMaxValue * mCurrentValue);
break;
case VALUE:
text = decimalFormat.format(mCurrentValue);
break;
}
// only re-calc position and size if string length changed
if (mTextLength != text.length()) {
update = true;
mTextLength = text.length();
if (mTextLength == 1) {
mOuterTextBounds = getInnerCircleRect(mCircleBounds);
mOuterTextBounds = new RectF(mOuterTextBounds.left + (mOuterTextBounds.width() * 0.1f), mOuterTextBounds.top, mOuterTextBounds.right - (mOuterTextBounds.width() * 0.1f), mOuterTextBounds.bottom);
} else {
mOuterTextBounds = getInnerCircleRect(mCircleBounds);
}
if (mIsAutoTextSize) {
setTextSizeAndTextBoundsWithAutoTextSize(unitGapWidthHalf, unitWidth, unitGapHeightHalf, unitHeight, text);
} else {
setTextSizeAndTextBoundsWithFixedTextSize(text);
}
}
if (DEBUG) {
Paint rectPaint = new Paint();
rectPaint.setColor(Color.MAGENTA);
canvas.drawRect(mOuterTextBounds, rectPaint);
rectPaint.setColor(Color.GREEN);
canvas.drawRect(mActualTextBounds, rectPaint);
}
canvas.drawText(text, mActualTextBounds.left - (mTextPaint.getTextSize() * 0.02f), mActualTextBounds.bottom, mTextPaint);
if (mShowUnit) {
if (mIsAutoColorEnabled) {
mUnitTextPaint.setColor(calcTextColor(mCurrentValue));
}
if (update) {
//calc unit text position
if (mIsAutoTextSize) {
setUnitTextBoundsAndSizeWithAutoTextSize(unitGapWidthHalf, unitWidth, unitGapHeightHalf, unitHeight);
} else {
setUnitTextBoundsAndSizeWithFixedTextSize(unitGapWidthHalf * 2f, unitGapHeightHalf * 2f);
}
}
if (DEBUG) {
Paint rectPaint = new Paint();
rectPaint.setColor(Color.RED);
canvas.drawRect(mUnitBounds, rectPaint);
}
canvas.drawText(mUnit, mUnitBounds.left - (mUnitTextPaint.getTextSize() * 0.02f), mUnitBounds.bottom, mUnitTextPaint);
}
}
private void drawBar(Canvas _canvas, float _degrees) {
float startAngle = mDirection == Direction.CW ? mStartAngle : mStartAngle - _degrees;
if (!mShowBlock) {
if (mBarStrokeCap != Paint.Cap.BUTT && _degrees > 0 && mBarColors.length > 1) {
if (_degrees > 180) {
_canvas.drawArc(mCircleBounds, startAngle, _degrees / 2, false, mBarPaint);
_canvas.drawArc(mCircleBounds, startAngle, 1, false, mShaderlessBarPaint);
_canvas.drawArc(mCircleBounds, startAngle + (_degrees / 2), _degrees / 2, false, mBarPaint);
} else {
_canvas.drawArc(mCircleBounds, startAngle, _degrees, false, mBarPaint);
_canvas.drawArc(mCircleBounds, startAngle, 1, false, mShaderlessBarPaint);
}
} else {
_canvas.drawArc(mCircleBounds, startAngle, _degrees, false, mBarPaint);
}
} else {
drawBlocks(_canvas, mCircleBounds, startAngle, _degrees, false, mBarPaint);
}
}
//endregion draw
//----------------------------------
/**
* Turn off spinning mode
*/
public void stopSpinning() {
setSpin(false);
mAnimationHandler.sendEmptyMessage(AnimationMsg.STOP_SPINNING.ordinal());
}
/**
* Puts the view in spin mode
*/
public void spin() {
setSpin(true);
mAnimationHandler.sendEmptyMessage(AnimationMsg.START_SPINNING.ordinal());
}
private void setSpin(boolean spin) {
mSpin = spin;
}
//----------------------------------
//region touch input
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (mSeekModeEnabled == false) {
return super.onTouchEvent(event);
}
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP: {
mTouchEventCount = 0;
PointF point = new PointF(event.getX(), event.getY());
float angle = getRotationAngleForPointFromStart(point);
setValueAnimated(mMaxValue / 360f * angle, 800);
return true;
}
case MotionEvent.ACTION_MOVE: {
mTouchEventCount++;
if (mTouchEventCount > 5) { //touch/move guard
PointF point = new PointF(event.getX(), event.getY());
float angle = getRotationAngleForPointFromStart(point);
setValue(mMaxValue / 360f * angle);
return true;
} else {
return false;
}
}
case MotionEvent.ACTION_CANCEL:
mTouchEventCount = 0;
return false;
}
return super.onTouchEvent(event);
}
private float getRotationAngleForPointFromStart(PointF point) {
long angle = Math.round(calcRotationAngleInDegrees(mCenter, point));
float fromStart = mDirection == Direction.CW ? angle - mStartAngle : mStartAngle - angle;
return normalizeAngle(fromStart);
}
//endregion touch input
//----------------------------------
//-----------------------------------
//region listener for progress change
public interface OnProgressChangedListener {
void onProgressChanged(float value);
}
//endregion listener for progress change
//--------------------------------------
}